package sk.stuba.fiit.ltasys.web.mvc.model.service.core;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.bouncycastle.tsp.TimeStampResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import sk.stuba.fiit.ltasys.web.mvc.model.entity.ArchivePackageEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.CertificateEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.CrlEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.EvidenceRecordEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.service.CertificateService;
import sk.stuba.fiit.ltasys.web.mvc.model.service.CrlService;
import sun.security.x509.X509CRLImpl;

@Service("archiveSignature")
public class ArchiveSignatureVerifier{

	@Autowired
	private CertificateService certificateService;
	
	@Autowired
	private CrlService crlService;
	
	
	public void verifieArchiveSignature(ArchivePackageEntity archiv) throws Exception{
		
		List<EvidenceRecordEntity> list = new ArrayList<EvidenceRecordEntity>(archiv.getArchiveRecord());
		Collections.sort(list);
		Set<X509Certificate> allCertificates = new HashSet<X509Certificate>();
		
		EvidenceRecordEntity oldActual = null;
		for (int i = list.size()-1; i >= 0; i--) {
			EvidenceRecordEntity actual = list.get(i);
			byte[] hash = comuteHashRoot(archiv, actual);
			TimeStampResponse tsr = new TimeStampResponse(actual.getTimestamp().getTimestamp()); 
			byte[] tstHash = tsr.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest();
			
			if(!new String(hash).equals(new String(tstHash))){
				throw new Exception("Incorect hash in timestamp");
			}
			
			CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
			
			if(list.size()-1 == i){
				BigInteger sn = tsr.getTimeStampToken().getTimeStampInfo().getSerialNumber();
				CertificateEntity certificateEntity = certificateService.handleCertificate(sn);
				InputStream in = new ByteArrayInputStream(certificateEntity.getCert());		
				CrlEntity crlEntity = crlService.handleCrls(null);

				X509CRL crl = new X509CRLImpl(crlEntity.getCrl());
				X509Certificate certificate = (X509Certificate)certFactory.generateCertificate(in);
				
				X509CRLEntry entry = crl.getRevokedCertificate(certificate);
				if(entry != null ){
					throw new Exception("Revocated certificate");
				}
				tsr.getTimeStampToken().validate(certificate, "BC");
				allCertificates.add(certificate);
				oldActual = actual;
			}else{
				List<CertificateEntity> certificates = new ArrayList<CertificateEntity>(oldActual.getCertificates());
				List<CrlEntity> crls = new ArrayList<CrlEntity>(oldActual.getCrls());
				
				X509Certificate cerificate = null;
				BigInteger snCert = tsr.getTimeStampToken().getTimeStampInfo().getSerialNumber();
				for (CertificateEntity certEntity : certificates) {
					if(certEntity.getSerialNumber().equals(snCert)){
						InputStream in = new ByteArrayInputStream(certEntity.getCert());		
						cerificate =  (X509Certificate)certFactory.generateCertificate(in);	
					}
				}
				if(cerificate == null){
					throw new Exception("Certificate is not in evidence record");
				}
				X509CRL crl = new X509CRLImpl(actual.getCrls().iterator().next().getCrl());
				
				X509CRLEntry entry = crl.getRevokedCertificate(cerificate);
				if(entry != null ){
					throw new Exception("Revocated certificate");
				}
				tsr.getTimeStampToken().validate(cerificate, "BC");
				allCertificates.add(cerificate);
				oldActual = actual;
			}
		}
		
		for (X509Certificate x509Certificate : allCertificates) {
			createAndValidateCertificatePath(x509Certificate);		
		}
	}
	
	private void createAndValidateCertificatePath( X509Certificate x509Certificate) throws Exception {
		ArrayList<X509Certificate> certificateChain = new ArrayList<X509Certificate>();
		certificateChain.add(x509Certificate);
		while(!x509Certificate.getIssuerDN().equals(x509Certificate.getSubjectDN())){
			CertificateEntity certEntity = certificateService.handleCertificate(x509Certificate.getIssuerDN().toString());
			CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
			InputStream in = new ByteArrayInputStream(certEntity.getCert());		
			x509Certificate = (X509Certificate)certFactory.generateCertificate(in);
			certificateChain.add(x509Certificate);
		}
		
		for (int i = 0; i < certificateChain.size()-1; i++) {
			certificateChain.get(i).verify(certificateChain.get(i+1).getPublicKey());
		}
		certificateChain.get(certificateChain.size()-1).verify(certificateChain.get(certificateChain.size()-1).getPublicKey());
		if(!isTrustCertificate(certificateChain.get(certificateChain.size()-1))){
			throw new Exception();
		}
	}

	private boolean isTrustCertificate(X509Certificate x509Certificate) throws Exception {
		CertificateEntity certEntity = certificateService.handleCertificate(x509Certificate.getSerialNumber());
		CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
		InputStream in = new ByteArrayInputStream(certEntity.getCert());		
		X509Certificate x509DbCert = (X509Certificate)certFactory.generateCertificate(in);
		if(x509DbCert.equals(x509Certificate)){
			return certEntity.isTrustedCert();
		}else{
			return false;
		}
	}

	private byte [] comuteHashRoot(ArchivePackageEntity archiv, EvidenceRecordEntity evidence) throws Exception{
		List<EvidenceRecordEntity> list = new ArrayList<EvidenceRecordEntity>(archiv.getArchiveRecord());
		Collections.sort(list);
		
		//najstarsi zaznam
		if(list.get(0).equals(evidence) ){
			 List<byte[]> referenceData = new ArrayList<byte[]>();
			 //pridam obsah podpisu a obsah dat
//			 referenceData.add(archiv.getUserData().getSignature().getBytes());
			 for(int i= 0; i< archiv.getUserData().getData().size(); i++){
				 referenceData.add(archiv.getUserData().getData().get(i).getData());
			 }
			 byte[] hashReference = computeNodeHash(referenceData); 
			 
			 //pridam aktualny certifikat
			 List<byte[]> protectionData = new ArrayList<byte[]>();
			 for (CertificateEntity certificate : evidence.getCertificates()) {
				 protectionData.add(certificate.getCert());
			 }
			 for (CrlEntity crl: evidence.getCrls()){
				 protectionData.add(crl.getCrl());
			 }			 
			 
			 byte[] hashProtectionData = computeNodeHash(protectionData);			 
			 List<byte[]> timestampData = new ArrayList<byte[]>();
				timestampData.add(hashReference);
				timestampData.add(hashProtectionData);
				
			return computeNodeHash(timestampData);
		}else{
			byte[] hashReference = comuteHashRoot(archiv, list.get(list.indexOf(evidence)-1));
			 
			 List<byte[]> protectionData = new ArrayList<byte[]>();
			 for (CertificateEntity certificate : evidence.getCertificates()) {
				 protectionData.add(certificate.getCert());
			 }
			 for (CrlEntity crl: evidence.getCrls()){
				 protectionData.add(crl.getCrl());
			 }		
			 
			 protectionData.add(list.get(list.indexOf(evidence)-1).getTimestamp().getTimestamp());
			 
			 byte[] hashProtectionData = computeNodeHash(protectionData);
			
			 List<byte[]> timestampData = new ArrayList<byte[]>();
			 timestampData.add(hashReference);
			 timestampData.add(hashProtectionData);	
			 return computeNodeHash(timestampData);
		}
	}
	
	private static byte[] computeNodeHash(List<byte[]> unorderedByteList) throws Exception{
		TreeSet<String> orderedData = new TreeSet<String>();
		MessageDigest hash = MessageDigest.getInstance("SHA", "BC");		
		
		for (byte[] data : unorderedByteList) {
			hash.reset();
			hash.update(data);
			orderedData.add(new String(hash.digest()));
		}
		hash.reset();
		for (String hashData : orderedData) {
			hash.update(hashData.getBytes());
		}
		byte [] result = hash.digest();
		return result;
	}
}
